﻿
using EmperialApps.WeatherSpark.Agent;
using EmperialApps.WeatherSpark.Data;
using EmperialApps.WeatherSpark.Helpers;
using EmperialApps.WeatherSpark.Resources;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.ServiceModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;


namespace EmperialApps.WeatherSpark.Internal {

    using ProgressMessage = Pair<string, bool>;

    internal static partial class Extensions {

        /// <summary>Separates a collection of values into those that are and are not multiples of the specified value.</summary>
        /// <returns>A collection of values that are multiples of <paramref name="multiple"/>, and a collection of the remaining values.</returns>
        public static Pair<List<double>, List<double>> SeparateMultiples( this IEnumerable<double> values, double multiple ) {
            var multiples = new List<double>( );
            var nonMultiples = new List<double>( );

            foreach( double value in values ) {
                var list = value % multiple == 0 ? multiples : nonMultiples;
                list.Add( value );
            }

            return Pair.Create( multiples, nonMultiples );
        }

        /// <summary>Creates a rotation matrix around the specified origin.</summary>
        public static Matrix CreateRotationMatrix( double angle, Point origin ) {
            double sine = Math.Sin( angle );
            double cosine = Math.Cos( angle );
            return new Matrix(
                cosine, sine,
                -sine, cosine,
                origin.X - origin.X * cosine + origin.Y * sine,
                origin.Y - origin.Y * cosine - origin.X * sine );
        }

        /// <summary>Ensures a value close to zero is displayed with a non-zero value, to the specified number of digits.</summary>
        public static bool DistinguishZero( ref double value, int digits ) {
            double rounded = Math.Round( value, digits );
            if( rounded == 0 ) {
                double factor = Math.Pow( 10, digits );
                value = Math.Ceiling( value * factor ) / factor;
            }

            return value == 0;
        }


        #region Animation

        /// <summary>Creates an animation for the specified property on a target object.</summary>
        public static DoubleAnimation Animate( this DependencyObject target, string propertyName, EasingMode easingMode, double? to = null, TimeSpan? beginTime = null ) {
            var animation = new DoubleAnimation { EasingFunction = new CubicEase { EasingMode = easingMode }, To = to, BeginTime = beginTime };
            Storyboard.SetTarget( animation, target );
            Storyboard.SetTargetProperty( animation, new PropertyPath( propertyName ) );
            return animation;
        }

        /// <summary>Animates the insertion of an element into a panel.</summary>
        public static void Insertimate( this StackPanel panel, UIElement element, int index ) {
            QueueAnimation( InsertimateCore, panel, element, index, fade: true );
        }

        /// <summary>Animates the vertical movement of an element within a panel.</summary>
        public static void Movimate( this StackPanel panel, UIElement element, int index ) {
            QueueAnimation( MovimateCore, panel, element, index, fade: false );
        }


        private static readonly Action PlaceholderAction = delegate { };

        private static readonly DependencyProperty PendingProperty =
            DependencyProperty.RegisterAttached(
                "Pending", typeof( Action ), typeof( Extensions ),
                new PropertyMetadata(
                    default( Action )
                )
            );

        private static void QueueAnimation( Func<StackPanel, UIElement, int, bool, Storyboard> animation, StackPanel panel, UIElement element, int index, bool fade ) {
            Canvas animationCanvas = null;
            for( int i = 0; animationCanvas == null && i < panel.Children.Count; ++i ) {
                Canvas canvas = panel.Children[i] as Canvas;
                if( canvas != null && canvas.GetValue( PendingProperty ) != null )
                    animationCanvas = canvas;
            }

            // If a previous animation is already in progress, queue new animation to occur after.
            if( animationCanvas != null ) {
                Action pending = (Action)animationCanvas.GetValue( PendingProperty );
                pending += delegate { QueueAnimation( animation, panel, element, index, fade ); };
                animationCanvas.SetValue( PendingProperty, pending );
            }
            // Otherwise, start immediately.
            else {
                animation( panel, element, index, fade );
            }
        }

        private static Storyboard InsertimateCore( StackPanel panel, UIElement element, int index, bool fade ) {
            // Add element to end of panel to perform layout.
            panel.Children.Add( element );
            panel.UpdateLayout( );

            // Fade in element at new position.
            return MovimateCore( panel, element, index, fade );
        }

        private static Storyboard MovimateCore( StackPanel panel, UIElement element, int index, bool fade ) {
            // Get positions that item will traverse.
            UIElement target = panel.Children[index];
            GeneralTransform fromTransfrom = panel.TransformToVisual( element );
            GeneralTransform toTransform = panel.TransformToVisual( target );
            Point fromPoint = fromTransfrom.Transform( new Point( ) );
            Point toPoint = toTransform.Transform( new Point( ) );

            // Move item into animation canvas at target position, with transform at source position.
            double sourceHeight = element.DesiredSize.Height;
            Canvas sourceCanvas = new Canvas { Height = sourceHeight };
            Canvas targetCanvas = new Canvas { Height = 0 };
            panel.Children.Insert( index, targetCanvas );
            panel.Children.Insert( panel.Children.IndexOf( element ), sourceCanvas ); // (insert may change order)
            panel.Children.Remove( element );
            targetCanvas.Children.Add( element );
            targetCanvas.SetValue( PendingProperty, PlaceholderAction );

            DoubleAnimation targetAnimation;
            if( fade ) {
                targetCanvas.Opacity = 0;
                targetAnimation = targetCanvas.Animate( "Opacity", EasingMode.EaseIn, 1, TimeSpan.FromSeconds( 0.2 ) );
            }
            else {
                var transform = new TranslateTransform { Y = toPoint.Y - fromPoint.Y };
                targetAnimation = transform.Animate( "Y", EasingMode.EaseIn, 0 );
                targetCanvas.RenderTransform = transform;
            }

            // Animate item moving to new destination.
            var sourceHeightAnimation = sourceCanvas.Animate( "Height", EasingMode.EaseIn, 0 );
            var targetHeightAnimation = targetCanvas.Animate( "Height", EasingMode.EaseIn, sourceHeight );
            var storyboard = new Storyboard { Children = { targetAnimation, sourceHeightAnimation, targetHeightAnimation } };

            storyboard.Completed += delegate {
                // Move item directly into panel, and remove animation canvases.
                targetCanvas.Children.Remove( element );
                panel.Children.Insert( panel.Children.IndexOf( targetCanvas ), element );
                panel.Children.Remove( sourceCanvas );
                panel.Children.Remove( targetCanvas );

                // Begin any pending animations.
                Action pending = (Action)targetCanvas.GetValue( PendingProperty );
                pending( );
            };

            storyboard.Begin( );
            return storyboard;
        }

        #endregion

        #region Navigation

        private const string CurrentSettingsIndexName = "Index";

        public static void RegisterTranstions( this PhoneApplicationPage page ) {
            TransitionService.SetNavigationInTransition( page, new NavigationInTransition {
                Backward = new TurnstileTransition { Mode = TurnstileTransitionMode.BackwardIn },
                Forward = new TurnstileTransition { Mode = TurnstileTransitionMode.ForwardIn }
            } );
            TransitionService.SetNavigationOutTransition( page, new NavigationOutTransition {
                Backward = new TurnstileTransition { Mode = TurnstileTransitionMode.BackwardOut },
                Forward = new TurnstileTransition { Mode = TurnstileTransitionMode.ForwardOut }
            } );
        }

        public static Uri GetIndexedPageAddress( this Uri source, int index ) {
            string queryAddress = source.OriginalString + "?" + CurrentSettingsIndexName + "=" + index;
            return new Uri( queryAddress, UriKind.Relative );
        }

        public static EventHandler NavigateTo( this PhoneApplicationPage page, Uri source, int? index = null ) {
            string sourceName = source.ToString( );
            int nameStart = 1 + sourceName.LastIndexOf( '/' );
            int nameLength = sourceName.Length - nameStart - "Page.xaml".Length;
            string logMessage = sourceName.Substring( nameStart, nameLength ) + " menu clicked";

            Uri address =
                index.HasValue
                    ? GetIndexedPageAddress( source, index.Value )
                    : source;

            return delegate {
                page.GetType( ).Log( logMessage );
                page.NavigationService.Navigate( address );
            };
        }

        public static bool EnsureCurrentSettings( this PhoneApplicationPage page, bool fallback ) {
            if( Settings.Current != null )
                return false;

            string indexInput;
            if( page.NavigationContext.QueryString.TryGetValue( CurrentSettingsIndexName, out indexInput ) )
                page.GetType( ).Log( "- Reading settings for forecast " + indexInput );
            else if( fallback )
                page.GetType( ).Log( "- Reading settings for default forecast" );
            else
                return false;

            int index = int.Parse( indexInput ?? "0" );
            Settings.Current = Settings.GetLocationSettings( index );

            return true;
        }

        #endregion

        #region Errors

        public static void Record( this Exception ex, string footer = "" ) {
            var log = Logger.GetLog( );
            string time = DateTimeOffset.Now.ToString( "o" );
            LittleWatson.RecordException( ex, "When: " + time, footer, log );
        }

        public static void Report( this Exception ex, Localized.ErrorSource source, string caller = null ) {
            string footer = new System.Diagnostics.StackTrace( ).ToString( );
            if( !string.IsNullOrEmpty( caller ) )
                footer = string.Format( "[{0}]{1}{2}", caller, Environment.NewLine, footer );

            Record( ex, footer );
            LittleWatson.CheckForPreviousException( source );
        }

        public static bool WasCancelled( this AsyncCompletedEventArgs e ) {
            return e.Cancelled
                || e.Error is CommunicationObjectAbortedException;
        }


        public static void Show( this Localized.Popup id, Localized.FormatArgument args = default(Localized.FormatArgument) ) {
            DisplayPopup( id, MessageBoxButton.OK, args );
        }

        public static MessageBoxResult Ask( this Localized.Popup id, Localized.FormatArgument args ) {
            return DisplayPopup( id, MessageBoxButton.OKCancel, args );
        }

        private static MessageBoxResult DisplayPopup( Localized.Popup id, MessageBoxButton button, Localized.FormatArgument args ) {
            string caption = AppResources.ResourceManager.GetString( "popup_" + id + "_caption" );
            string format = AppResources.ResourceManager.GetString( "popup_" + id + "_message" );
            string message = Localized.Format( format, args );

            return MessageBox.Show( message, caption, button );
        }

        #endregion

        #region Forecast Persistence

        public static string GetGeographicName( this Coordinate location, string separator = "  " ) {
            string latitudeSymbol = location.Latitude > 0 ? "N" : "S";
            string longitudeSymbol = location.Longitude > 0 ? "E" : "W";
            return string.Format(
                "{0}\u200a\u2060\u00b0{1}{2}{3}\u200a\u2060\u00b0{4}",
                Math.Abs( location.Latitude ), latitudeSymbol,
                separator,
                Math.Abs( location.Longitude ), longitudeSymbol );
        }

        public static void DeleteFile( this Coordinate location, Type type, int? index ) {
            type.Log( "Deleting forecast " + (index ?? Settings.IndexOfLocation( location )) + " file" );
            if( !SharedStorage.DeleteForecast( location ) )
                type.Log( "- Forecast file not found" );
        }

        public static Forecast SaveToFile( this Forecast forecast, Type type, int index ) {
            type.Log( "Saving forecast " + index + " to file" );
            try {
                type.Log( "- Checking for existing forecast" );
                return SharedStorage.UpdateForecast( forecast.Location, UpdateForecast, Pair.Create( forecast, type ) );
            }
            catch( Exception error ) {
                type.Log( "- Could not load existing forecast from disk:" + Environment.NewLine + error );
                SharedStorage.SaveForecast( forecast );
                return forecast;
            }
        }

        public static Forecast UpdateFromFile( this Forecast forecast, Type type, int index ) {
            Forecast updated;

            Forecast existing;
            Exception error;
            type.Log( "- Checking for existing forecast" );
            if( TryLoadForecast( forecast.Location, type, index, out existing, out error ) ) {
                updated = CombineForecasts( existing, forecast, type );
            }
            else {
                updated = forecast;
                if( error != null )
                    type.Log( "- Could not load existing forecast from disk:" + Environment.NewLine + error );
            }

            return updated;
        }

        public static bool TryLoadForecast( this Coordinate location, Type type, int index, out Forecast forecast, out Exception error ) {
            return TryLoadForecast( LoadFromFile, "file", location, type, index, out forecast, out error );
        }

        public static bool TryLoadForecast( this Stream stream, Coordinate fallbackLocation, Type type, int index, out Forecast forecast, out Exception error ) {
            return TryLoadForecast( LoadFromStream, "stream", Pair.Create( stream, fallbackLocation ), type, index, out forecast, out error );
        }


        private static Forecast LoadFromFile( Coordinate location, Type type, int index ) {
            Forecast forecast;
            if( !SharedStorage.TryLoadForecast( location, out forecast ) )
                type.Log( "- Forecast " + index + " file does not exist" );

            return forecast;
        }

        private static Forecast LoadFromStream( Pair<Stream, Coordinate> source, Type type, int index ) {
            Stream stream = source.Item1;
            Coordinate fallbackLocation = source.Item2;
            return Forecast.Load( stream, fallbackLocation );
        }

        private static bool TryLoadForecast<T>( Func<T, Type, int, Forecast> load, string sourceName, T source, Type type, int index, out Forecast forecast, out Exception error ) {
            type.Log( "Loading forecast " + index + " from " + sourceName );
            try {
                forecast = load( source, type, index );
                error = null;

                return forecast != null;
            }
            catch( Exception ex ) {
                type.Log( "- Failed to load forecast " + index + " from " + sourceName );
                forecast = null;
                error = ex;

                return false;
            }
        }


        private static Forecast UpdateForecast( bool wasRead, Forecast existing, Pair<Forecast, Type> updateData ) {
            return wasRead
                 ? CombineForecasts( existing, updateData.Item1, updateData.Item2 )
                 : updateData.Item1;
        }

        private static Forecast CombineForecasts( Forecast existing, Forecast forecast, Type type ) {
            type.Log( "- Combining new forecast with existing file from " + existing.Start.ToString( "yyyy-MM-dd HHmm zzz" ) );
            return SharedStorage.CombineForecasts( existing, forecast );
        }

        #endregion

        #region Progress

        public static bool HasFlag( this ProgressKind kind, ProgressKind flag ) {
            return (kind & flag) == flag;
        }

        public static bool IsProgressComplete( this PhoneApplicationPage page ) {
            ProgressIndicator progress = SystemTray.GetProgressIndicator( page );
            return progress == null
                || !progress.IsVisible
                || !progress.IsIndeterminate;
        }

        public static void SetProgress( this PhoneApplicationPage page, ProgressKind kind, Localized.Status status, Localized.FormatArgument args = default(Localized.FormatArgument) ) {
            string text, logMessage;
            GetProgressText( status, args, out text, out logMessage );
            SetProgressCore( page, logMessage, text, kind );
        }

        public static bool GetProgressText( this Localized.Status status, Localized.FormatArgument args, out string text, out string logMessage ) {
            if( status == 0 ) {
                text = logMessage = null;
            }
            else {
                string id, logId;
                if( status >= Localized.Status.SearchFound0 ) {
                    int count = status - Localized.Status.SearchFound0;
                    id = "Status_" + Localized.Status.SearchFound + count;
                    logId = "Log_" + Localized.Status.SearchFound;
                }
                else {
                    id = "Status_" + status;
                    logId = "Log_" + status;
                }

                string format = AppResources.ResourceManager.GetString( id );
                string logFormat = LogResources.ResourceManager.GetString( logId )
                                ?? AppResources.ResourceManager.GetString( id, LogResources.Culture );
                text = Localized.Format( format, args.StatusArgument );
                logMessage = Localized.Format( logFormat, args.LogArgument );
            }

            return status != 0;
        }

        private static void SetProgressCore( this PhoneApplicationPage page, string logMessage, string text, ProgressKind kind ) {
            ProgressIndicator progress = SystemTray.GetProgressIndicator( page );
            page.GetType( ).Log( logMessage ?? text );

            bool complete = kind.HasFlag( ProgressKind.Complete );
            bool important = kind.HasFlag( ProgressKind.Important );

            // If no progress message has been shown before, create new progress indicator.
            if( progress == null ) {
                progress = new ProgressIndicator( );
                SystemTray.SetProgressIndicator( page, progress );
            }
            // Otherwise, if an important message is being shown, save the new message as the fallback.
            else if( !important && page.GetValue( FallbackProgressProperty ) != null ) {
                page.SetValue( FallbackProgressProperty, new ProgressMessage( text, complete ) );
                progress.IsIndeterminate = !complete;
                return;
            }

            // End the current progress message, if any.
            var progressStoryboard = (Storyboard)progress.GetValue( ProgressStoryboardProperty );
            if( progressStoryboard != null )
                progressStoryboard.Stop( );

            // Set the new progress message.
            progress.Text = text ?? "";
            progress.IsVisible = text != null;
            progress.IsIndeterminate = !complete;

            // If the new message is important, save it as the fallback.
            if( important )
                page.SetValue( FallbackProgressProperty, new ProgressMessage( text, complete ) );

            // If the message will complete, begin the storyboard.
            if( (important || complete) && text != null ) {
                if( progressStoryboard == null ) {
                    var visibleKeyframe = new DiscreteObjectKeyFrame { Value = true, KeyTime = KeyTime.FromTimeSpan( TimeSpan.Zero ) };
                    var hiddenKeyFrame = new DiscreteObjectKeyFrame { Value = false, KeyTime = KeyTime.FromTimeSpan( TimeSpan.FromSeconds( 2 ) ) };
                    var isVisibleAnimation = new ObjectAnimationUsingKeyFrames { KeyFrames = { visibleKeyframe, hiddenKeyFrame } };
                    Storyboard.SetTarget( isVisibleAnimation, progress );
                    Storyboard.SetTargetProperty( isVisibleAnimation, new PropertyPath( "IsVisible" ) );

                    progressStoryboard = new Storyboard { Children = { isVisibleAnimation } };
                    progressStoryboard.Completed += ( s, e ) => ProcessFallbackMessage( page );
                    progress.SetValue( ProgressStoryboardProperty, progressStoryboard );
                }

                progressStoryboard.Begin( );
            }
        }


        private static void ProcessFallbackMessage( PhoneApplicationPage page ) {
            string messageText;
            bool messageComplete;
            if( page.GetValue( FallbackProgressProperty ).TryGetValues( out messageText, out messageComplete ) ) {
                page.ClearValue( FallbackProgressProperty );

                // If fallback is incomplete or different from current message, show it.
                if( !messageComplete || messageText != SystemTray.GetProgressIndicator( page ).Text )
                    SetProgressCore( page, "", messageText, messageComplete ? ProgressKind.Complete : ProgressKind.Incomplete );
            }
        }

        private static readonly DependencyProperty FallbackProgressProperty =
            DependencyProperty.RegisterAttached(
                "FallbackProgress", typeof( ProgressMessage? ), typeof( Extensions ),
                new PropertyMetadata(
                    default( ProgressMessage? )
                )
            );

        private static readonly DependencyProperty ProgressStoryboardProperty =
            DependencyProperty.RegisterAttached(
                "ProgressStoryboard", typeof( Storyboard ), typeof( Extensions ),
                new PropertyMetadata(
                    default( Storyboard )
                )
            );

        #endregion

        #region Application Bar

        public static Uri ToImageUri( this string id ) {
            string image = "/Assets/AppBar/appbar." + id + ".png";
            return new Uri( image, UriKind.Relative );
        }

        public static void SetIcon( this ApplicationBarIconButton button, string id ) {
            button.IconUri = ToImageUri( id );
        }

        public static ApplicationBarIconButton AddIconButton( this IApplicationBar appBar, Localized.Button id, EventHandler clickHandler, bool isEnabled = true ) {
            string text = AppResources.ResourceManager.GetString( "appbar_" + id );
            var button = new ApplicationBarIconButton { Text = text, IsEnabled = isEnabled };
            button.SetIcon( id.ToString( ) );
            button.Click += clickHandler;

            appBar.Buttons.Add( button );
            return button;
        }

        public static ApplicationBarMenuItem AddMenuItem( this IApplicationBar appBar, Localized.Menu id, EventHandler clickHandler, bool isEnabled = true ) {
            string text = AppResources.ResourceManager.GetString( "menu_" + id );
            var menuItem = new ApplicationBarMenuItem { Text = text, IsEnabled = isEnabled };
            menuItem.Click += clickHandler;

            appBar.MenuItems.Add( menuItem );
            return menuItem;
        }

        #endregion

        #region Dependency Property Updates

        public static readonly DependencyPropertyAccessor<UIElement, TranslateTransform> RenderTransform =
                           new DependencyPropertyAccessor<UIElement, TranslateTransform>( "RenderTransform", clear: false );

        public static readonly DependencyPropertyAccessor<Geometry, TranslateTransform> Transform =
                           new DependencyPropertyAccessor<Geometry, TranslateTransform>( "Transform", clear: false );

        public static readonly DependencyPropertyAccessor<UIElement, RectangleGeometry> RectangleClip =
                           new DependencyPropertyAccessor<UIElement, RectangleGeometry>( "Clip", clear: false );

        public static readonly DependencyPropertyAccessor<UIElement, PathGeometry> PathClip =
                           new DependencyPropertyAccessor<UIElement, PathGeometry>( "Clip", clear: true );

        public static readonly DependencyPropertyAccessor<System.Windows.Shapes.Path, PathGeometry> PathData =
                           new DependencyPropertyAccessor<System.Windows.Shapes.Path, PathGeometry>( "Data", clear: true );


        public static TranslateTransform EnsureTranslateTransform( this UIElement target ) {
            return Ensure( target, RenderTransform );
        }

        public static void Translate( this UIElement target, DependencyProperty translateProperty, double value ) {
            var transform = EnsureTranslateTransform( target );
            transform.SetValue( translateProperty, value );
        }

        public static void Translate( this Geometry target, DependencyProperty translateProperty, double value ) {
            var transform = Ensure( target, Transform );
            transform.SetValue( translateProperty, value );
        }


        public static P Ensure<T, P>( this T target, DependencyPropertyAccessor<T, P> accessor )
            where T : DependencyObject
            where P : class, new( ) {

            P value = target.GetValue( accessor.Property ) as P;
            if( value == null ) {
                value = new P( );
                target.SetValue( accessor.Property, value );
            }

            return value;
        }


        public static void Update<T, U, P>( this T target, DependencyPropertyAccessor<U, P> accessor, Action<T, P> update )
            where T : U
            where U : DependencyObject
            where P : class, new( ) {

            Update( target, accessor,
                update,
                ( t, p, u ) => { u( t, p ); return 0; } );
        }

        public static void Update<T, U, P, D>( this T target, DependencyPropertyAccessor<U, P> accessor, D data, Action<T, P, D> update )
            where T : U
            where U : DependencyObject
            where P : class, new( ) {

            Update( target, accessor,
                Pair.Create( update, data ),
                ( t, p, d ) => { d.Item1( t, p, d.Item2 ); return 0; } );
        }

        public static R Update<T, U, P, D, R>( this T target, DependencyPropertyAccessor<U, P> accessor, D data, Func<T, P, D, R> update )
            where T : U
            where U : DependencyObject
            where P : class, new( ) {

            if( target == null )
                return default( R );

            R result;
            if( !accessor.Clear ) {
                P value = Ensure( target, accessor );
                result = update( target, value, data );
            }
            else {
                P value =
                       target.GetValue( accessor.Property ) as P
                    ?? new P( );

                var pathGeometry = value as PathGeometry;
                if( pathGeometry != null )
                    pathGeometry.Figures.Clear( );

                target.SetValue( accessor.Property, null );
                {
                    result = update( target, value, data );
                }
                target.SetValue( accessor.Property, value );
            }

            return result;
        }


        public struct DependencyPropertyAccessor<T, P>
            where T : DependencyObject {

            public readonly string Name;
            public readonly bool Clear;
            public readonly DependencyProperty Property;

            public DependencyPropertyAccessor( string name, bool clear )
                : this( name, clear, GetDependencyProperty( name ) ) { }

            private DependencyPropertyAccessor( string name, bool clear, DependencyProperty property ) {
                this.Name = name;
                this.Clear = clear;
                this.Property = property;
            }

            public override string ToString( ) {
                return this.Name;
            }

            private static DependencyProperty GetDependencyProperty( string name ) {
                var field = typeof( T ).GetField( name + "Property", BindingFlags.Public | BindingFlags.Static );
                var value = field.GetValue( null );
                return (DependencyProperty)value;
            }
        }

        #endregion

    }

}
